package za.ac.sun.cs.distrib.logging;

import gov.nasa.jpf.Config;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;

import org.apfloat.Apint;

import redis.clients.jedis.Jedis;

public class LogWriter {

	private static final String LOG_DIRECTORY = System.getProperty("java.io.tmpdir");

	private static final String LOG_DATE = new SimpleDateFormat("yyyyMMdd-HHmmss-SSS").format(new Date());
	
	private static final int LOG_RANDOM = new Random().nextInt(9);

	private static final String LOG_FILENAME = String.format("%s/%s-%s.txt", LOG_DIRECTORY, LOG_DATE, LOG_RANDOM);

	private static int logLevel = 0;

	private static final Set<LogDestination> destinations = new HashSet<LogWriter.LogDestination>();

	public static void initialize(Config config, String id) {
		String level = config.getProperty("search.TreeListener.log", "all");
		if (level.equals("all")) {
			logLevel = 2;
		} else if (level.equals("some")) {
			logLevel = 1;
		} else {
			logLevel = 0;
		}
		
		// Try to create the Jedis logger
		String logHost = config.getString("search.TreeListener.log.host");
		if (logHost != null) {
			int logPort = config.getInt("search.TreeListener.log.port", 7711);
			JedisDestination d = new JedisDestination(logHost, logPort, id);
			destinations.add(d);
			System.out.println("Sending log to redis " + logHost + ":" + logPort + " (" + id + ")");
		}

		// Try to create the file logger
		boolean logToFile = config.getBoolean("search.TreeListener.logToFile", false);
		if (logToFile) {
			try {
				FileDestination d = new FileDestination(LOG_FILENAME);
				destinations.add(d);
				System.out.println("Writing log to " + LOG_FILENAME);
			} catch (IOException x) {
				x.printStackTrace();
			}
		}
		
		if (destinations.size() == 0) {
			logLevel = 0;
		}
		if (logLevel > 0) {
			StringBuilder b = new StringBuilder();
			b.append(System.currentTimeMillis());
			b.append("@@IDENTIFY");
			try {
				b.append("@@").append(InetAddress.getLocalHost().getHostAddress());
			} catch (UnknownHostException x) {
				b.append("@@0.0.0.0");
			}
			b.append("@@").append(ManagementFactory.getRuntimeMXBean().getName());
			log(b.toString());
		}
	}

	public void shutdown() {
		for (LogDestination d : destinations) {
			d.shutdown();
		}
	}

	private static void log(String message) {
		for (LogDestination d : destinations) {
			d.write(message);
		}
	}
	
	public static void nochoice(String ifPC, String elsePC) {
		if (logLevel > 1) {
			StringBuilder b = new StringBuilder();
			b.append(System.currentTimeMillis());
			b.append("@@NOCHOICE");
			b.append("@@").append(ifPC);
			b.append("@@").append(elsePC);
			log(b.toString());
		}
	}

	public static void choice(String taken, Apint takenCount, String notTaken,
			Apint notTakenCount) {
		if (logLevel > 1) {
			StringBuilder b = new StringBuilder();
			b.append(System.currentTimeMillis());
			b.append("@@CHOICE");
			b.append("@@").append(taken);
			b.append("@@").append(takenCount);
			b.append("@@").append(notTaken);
			b.append("@@").append(notTakenCount);
			log(b.toString());
		}
	}

	public static void choice(String taken, String notTaken, boolean notTakenAvail) {
		if (logLevel > 1) {
			StringBuilder b = new StringBuilder();
			b.append(System.currentTimeMillis());
			b.append("@@CHOICE");
			b.append("@@").append(taken);
			b.append("@@").append(true);
			b.append("@@").append(notTaken);
			b.append("@@").append(notTakenAvail);
			log(b.toString());
		}
	}

	public static void stats(Apint totalPaths, Apint totalValues,
			Apint totalOverlaps, Apint totalUpdates, Apint totalChoices,
			Apint totalDeadends, long cpuTime) {
		if (logLevel > 1) {
			StringBuilder b = new StringBuilder();
			b.append(System.currentTimeMillis());
			b.append("@@STATS");
			b.append("@@").append(totalPaths);
			b.append("@@").append(totalValues);
			b.append("@@").append(totalOverlaps);
			b.append("@@").append(totalUpdates);
			b.append("@@").append(totalChoices);
			b.append("@@").append(totalDeadends);
			b.append("@@").append(cpuTime);
			String message = b.toString();
			for (LogDestination d : destinations) {
				d.writeStats(message);
			}
		}
	}

	public static void stats(long totalPaths, long totalOverlaps, long totalUpdates, long totalChoices,
			long totalDeadends, long cpuTime) {
		if (logLevel > 1) {
			StringBuilder b = new StringBuilder();
			b.append(System.currentTimeMillis());
			b.append("@@STATS");
			b.append("@@").append(totalPaths);
			b.append("@@0");
			b.append("@@").append(totalOverlaps);
			b.append("@@").append(totalUpdates);
			b.append("@@").append(totalChoices);
			b.append("@@").append(totalDeadends);
			b.append("@@").append(cpuTime);
			String message = b.toString();
			for (LogDestination d : destinations) {
				d.writeStats(message);
			}
		}
	}
	
	public static void newcount(String pc, Apint count) {
		if (logLevel > 1) {
			StringBuilder b = new StringBuilder();
			b.append(System.currentTimeMillis());
			b.append("@@COUNT");
			b.append("@@").append(pc);
			b.append("@@").append(count);
			log(b.toString());
		}
	}

	public static void newavail(String pc, boolean isAvail) {
		if (logLevel > 1) {
			StringBuilder b = new StringBuilder();
			b.append(System.currentTimeMillis());
			b.append("@@AVAIL");
			b.append("@@").append(pc);
			b.append("@@").append(isAvail);
			log(b.toString());
		}
	}
	
	public static void conflict(String pc, Apint pre, Apint delta, Apint post) {
		if (logLevel > 1) {
			StringBuilder b = new StringBuilder();
			b.append(System.currentTimeMillis());
			b.append("@@CONFLICT");
			b.append("@@").append(pc);
			b.append("@@").append(pre);
			b.append("@@").append(delta);
			b.append("@@").append(post);
			log(b.toString());
		}
	}

	public static void conflict(String pc) {
		if (logLevel > 1) {
			StringBuilder b = new StringBuilder();
			b.append(System.currentTimeMillis());
			b.append("@@CONFLICT");
			b.append("@@").append(pc);
			log(b.toString());
		}
	}
	
	public static void update(String pc, Apint pre, Apint delta, Apint post) {
		if (logLevel > 1) {
			StringBuilder b = new StringBuilder();
			b.append(System.currentTimeMillis());
			b.append("@@UPDATE");
			b.append("@@").append(pc);
			b.append("@@").append(pre);
			b.append("@@").append(delta);
			b.append("@@").append(post);
			log(b.toString());
		}
	}

	public static void update(String pc) {
		if (logLevel > 1) {
			StringBuilder b = new StringBuilder();
			b.append(System.currentTimeMillis());
			b.append("@@UPDATE");
			b.append("@@").append(pc);
			log(b.toString());
		}
	}
	
	public static void newvar(String name, int min, int max) {
		if (logLevel > 1) {
			StringBuilder b = new StringBuilder();
			b.append(System.currentTimeMillis());
			b.append("@@NEWVAR");
			b.append("@@").append(name);
			b.append("@@").append(min);
			b.append("@@").append(max);
			log(b.toString());
		}
	}

	public static void done(long timeSpent) {
		if (logLevel > 0) {
			StringBuilder b = new StringBuilder();
			b.append(System.currentTimeMillis());
			b.append("@@DONE");
			b.append("@@").append(timeSpent);
			String message = b.toString();
			for (LogDestination d : destinations) {
				d.writeDone(message);
			}
		}
	}

	public static void greenStats(String result) {
		if (logLevel > 0) {
			StringBuilder b = new StringBuilder();
			b.append(System.currentTimeMillis());
			b.append("@@GREENSTATS");
			b.append("@@").append(result);
			String message = b.toString();
			for (LogDestination d : destinations) {
				d.writeGreenStats(message);
			}
		}
	}
	
	private interface LogDestination {
		public void write(String string);
		public void writeStats(String string);
		public void writeGreenStats(String string);
		public void writeDone(String string);
		public void shutdown();
	}

	private static class JedisDestination implements LogDestination {

		private final Jedis db;

		private final String entryKeyPrefix;
		
		private int entryCounter = 0;

		public JedisDestination(String logHost, int logPort, String id) {
			db = new Jedis(logHost, logPort);
			Random r = new Random();
			StringBuilder b = new StringBuilder();
			for (int i = 0; i < 4; i++) {
				b.append((char) ('A' + r.nextInt(26)));
				b.append((char) ('a' + r.nextInt(26)));
				b.append((char) ('0' + r.nextInt(10)));
			}
			entryKeyPrefix = b.toString();
			db.lpush("log:" + id, entryKeyPrefix);
		}

		@Override
		public void write(String message) {
			StringBuilder b = new StringBuilder();
			b.append(entryKeyPrefix);
			b.append(':').append(entryCounter++);
			db.set(b.toString(), message);
		}
		
		@Override
		public void writeStats(String message) {
			StringBuilder b = new StringBuilder();
			b.append(entryKeyPrefix);
			b.append(":stats");
			db.set(b.toString(), message);
			write(message);
		}
		
		@Override
		public void writeGreenStats(String message) {
			StringBuilder b = new StringBuilder();
			b.append(entryKeyPrefix);
			b.append(":greenstats");
			db.set(b.toString(), message);
			write(message);
		}
		
		@Override
		public void writeDone(String message) {
			StringBuilder b = new StringBuilder();
			b.append(entryKeyPrefix);
			b.append(":done");
			db.set(b.toString(), message);
			write(message);
		}
		
		@Override
		public void shutdown() {
			db.shutdown();
		}

	}

	private static class FileDestination implements LogDestination {

		private final BufferedWriter writer;

		public FileDestination(String logFilename) throws IOException {
			File f = new File(logFilename);
			FileWriter w = new FileWriter(f);
			writer = new BufferedWriter(w);
		}

		@Override
		public void write(String message) {
			try {
				writer.write(message);
			} catch (IOException x) {
				x.printStackTrace();
			}
		}

		@Override
		public void writeStats(String message) {
			write(message);
		}
		
		@Override
		public void writeGreenStats(String message) {
			write(message);
		}
		
		@Override
		public void writeDone(String message) {
			write(message);
		}
		
		@Override
		public void shutdown() {
			try {
				writer.close();
			} catch (IOException x) {
				x.printStackTrace();
			}
		}

	}

}
